﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Reflection;
using Microsoft.CSharp;
using System.CodeDom.Compiler;

namespace CSharpExperiments.Scripting
{
	// TODO: need to be able to list referenced types, not just assemblies
	// TODO: need to provide simple implementations publicly, cleanup public implementations
	// TODO: need to impl decorator pattern so preparsers/repos/compilers can be composed
	public static class Scripts
	{
		/// <summary>Creates a simple C# script compiler, reading files from a directory,
		/// referencing extraAssemblies (should include assemblies in your project).</summary>
		public static ScriptCache<T> Create<T>(String directory, List<String> extraAssemblies, bool debuggable)
		{
			return new ScriptCache<T>(
				new DirectoryRepo(directory),
				new SimplePreparser(extraAssemblies),
				new SimpleCSharpCompiler(debuggable));
		}

		/// <summary>Creates a simple C# script compiler, reading files from given resolver,
		/// referencing extraAssemblies (should include assemblies in your project).</summary>
		public static ScriptCache<T> Create<T>(Func<String, String> resolver, List<String> extraAssemblies, bool debuggable)
		{
			return new ScriptCache<T>(
				new DelegateRepo() { Func = resolver },
				new SimplePreparser(extraAssemblies),
				new SimpleCSharpCompiler(debuggable));
		}

		public class DelegateRepo : Repo
		{
			public Func<String, String> Func { get; set; }

			public String GetSource(String name)
			{
				return Func(name);
			}
		}

		public class DirectoryRepo : Repo
		{
			public String RootDirectory { get; private set; }

			public DirectoryRepo(String rootDirectory) { RootDirectory = rootDirectory; }

			public String GetSource(String name)
			{
				name = name.Replace('/', Path.DirectorySeparatorChar);
				name = name.Replace('\\', Path.DirectorySeparatorChar);
				if (! name.EndsWith(".cs")) name += ".cs";
				name = Path.Combine(RootDirectory, name);
				return File.ReadAllText(name);
			}
		}

		public class TemplatePreparser : Preparser
		{
			/// <summary>Needs to have a "[[TEH CODEZ]]" in it somewhere to substitute the given source.
			/// Occurrances of "[[EH GOOID]]" will be replaced with the same guid to prevent name clashes.</summary>
			public String Template { get; set; }
			public Preparser DelegatePreparser { get; set; }

			public TemplatePreparser(Preparser delegatePreparser)
			{
				Template = "[[TEH CODEZ]]";
				DelegatePreparser = delegatePreparser;
			}

			public ScriptInfo Analyze(String source)
			{
				String guid = Guid.NewGuid().ToString("N");
				source = Template
					.Replace("[[EH GOOID]]", guid)
					.Replace("[[TEH CODEZ]]", source);
				return DelegatePreparser.Analyze(source);
			}
		}

		public class SimplePreparser : Preparser
		{
			public List<String> ExtraAssemblies { get; set; }

			public SimplePreparser(List<String> extraAssemblies) { ExtraAssemblies = extraAssemblies; }

			public ScriptInfo Analyze(String source)
			{
				ScriptInfo info = new ScriptInfo();
				info.Source = source;
				int nsIndex = source.IndexOf("namespace ");
				int cnIndex = source.IndexOf("class ", nsIndex);
				info.Namespace = source.Substring(nsIndex, source.IndexOfAny(new[]{'{', '/'}, nsIndex) - nsIndex).Trim();
				info.Namespace = info.Namespace.Trim();
				if (info.Namespace.StartsWith("namespace")) info.Namespace = info.Namespace.Substring("namespace".Length);
				info.Namespace = info.Namespace.Trim();
				String className = source.Substring(cnIndex, source.IndexOfAny(new[]{'{', ':', '/'}, cnIndex) - cnIndex).Trim();
				if (className.StartsWith("class")) className = className.Substring("class".Length);
				className = className.Trim();
				info.FullTypeName = info.Namespace + "." + className;
				info.AssemblyName = info.FullTypeName.Replace('.', '_') + "_" + Guid.NewGuid().ToString();
				info.Assemblies.Add("mscorlib.dll");
				info.Assemblies.Add("System.dll");
				info.Assemblies.Add("System.Core.dll");
				info.Assemblies.AddRange(ExtraAssemblies);
				return info;
			}
		}

		public class SimpleCSharpCompiler : Compiler
		{
			public bool GenerateDebugInfo { get; set; }

			public SimpleCSharpCompiler(bool debug) { GenerateDebugInfo = debug; }

			public Assembly Compile(ScriptInfo script)
			{
				var options = new Dictionary<String, String>{{"CompilerVersion", "v4.0"}};
				var provider = new CSharpCodeProvider(options);
				var parameters = new CompilerParameters(script.Assemblies.ToArray());
				parameters.GenerateInMemory = true;
				parameters.IncludeDebugInformation = GenerateDebugInfo;
				parameters.OutputAssembly = Path.Combine(Path.GetTempPath(), "SimpleCSharpCompiler", script.AssemblyName);

				if (! parameters.OutputAssembly.EndsWith(".dll"))
					parameters.OutputAssembly += ".dll";

				if (! Directory.Exists(Path.GetDirectoryName(parameters.OutputAssembly)))
					Directory.CreateDirectory(Path.GetDirectoryName(parameters.OutputAssembly));

				var results = provider.CompileAssemblyFromSource(parameters, script.Source);

				if (results.Errors.HasErrors)
					throw new ApplicationException("Script compiler failed.");

				return results.CompiledAssembly;
			}
		}
	}
}
